import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/**
 * Class to represent a grouping operation within a relational
 * algebra execution plan.
 * @author Jonathan
 *
 */
public class RAGrouping implements RAExpression, Cloneable {
	
	private RAExpression inputRA;
	
	private Map<String, TableData> catalog;
	private ArrayList<Expression> select;
	private Map<String, String> from;
	private Expression where;
	private String groupBy;

	public RAGrouping(Map<String, TableData> catalog, ArrayList<Expression> select, Map<String, String> from, String groupBy,
			RAExpression inputRA)
	{
		this.catalog = catalog;
		this.select = select;
		this.from = from;
		this.groupBy = groupBy;
		this.inputRA = inputRA;
	}

	protected RAGrouping clone()
	{
		try
		{
			return (RAGrouping) super.clone();
		}
		catch(Exception e)
		{
			throw new RuntimeException(e);
		}
	}

	@Override
	public RAOutput execute() throws IOException {
		if(DBConfig.DEBUG)
		{
			System.out.println("Executing RAGrouping: " + this.toString());
		}
		
		RAOutput input = null;
		
		String inputFile = null;
		String outputFile = null;
		String table = null;
				
		if(inputRA != null)
		{
			input = inputRA.execute();
			inputFile = input.outFile;
			outputFile = inputFile.replaceAll("\\.[sj]out", "") + ".gout";
		}
		else
		{
			for (String t : from.values())
			{
				table = t;
				inputFile = t + ".tbl";
				outputFile = t + ".gout";
			}
		}
		
		/*
		ArrayList <Attribute> inAtts = new ArrayList <Attribute> ();
	    inAtts.add (new Attribute ("Int", "o_orderkey"));
	    inAtts.add (new Attribute ("Int", "o_custkey"));
	    inAtts.add (new Attribute ("Str", "o_orderstatus"));
	    inAtts.add (new Attribute ("Float", "o_totalprice"));
	    inAtts.add (new Attribute ("Str", "o_orderdate"));
	    inAtts.add (new Attribute ("Str", "o_orderpriority"));
	    inAtts.add (new Attribute ("Str", "o_clerk"));
	    inAtts.add (new Attribute ("Int", "o_shippriority"));
	    inAtts.add (new Attribute ("Str", "o_comment"));
	    */
		ArrayList<Attribute> inAtts = null;
		if(table != null)
		{
			TableData td = catalog.get(table);
			for(String correlation : from.keySet())
			{
				inAtts = td.getTableLayout(correlation);
			}
		}
		else
		{
			inAtts = input.outAtts;
		}
	    
		/*
	    ArrayList <Attribute> outAtts = new ArrayList <Attribute> ();
	    outAtts.add (new Attribute ("Str", "att1"));
	    outAtts.add (new Attribute ("Str", "att2"));
	    outAtts.add (new Attribute ("Float", "att3"));
	    outAtts.add (new Attribute ("Int", "att4"));
		 */
		// Initialize the list of out attribute names and data types.
		int selectSize = select.size();
		ArrayList<Attribute> outAtts = new ArrayList<Attribute>(selectSize);
		for(int i = 0; i < selectSize; i++)
		{
			outAtts.add(i, new Attribute(select.get(i).getReturnType(from, catalog), "att" + i));
		}

	    /*
	    ArrayList <String> groupingAtts = new ArrayList <String> ();
	    groupingAtts.add ("o_orderdate");
	    groupingAtts.add ("o_orderstatus");
	    */
		if (DBConfig.DEBUG) System.out.println("RAGrouping setting up groupingAtts.");
		ArrayList <String> groupingAtts = new ArrayList<String>();
		if(groupBy != null)
		{
			if (DBConfig.DEBUG) System.out.println("groupBy != null, groupBy = \"" + groupBy + "\".");
			//groupingAtts.add(CorrelatedAttribute.parseCorrelatedAttribute(groupBy).getAttribute());
			groupingAtts.add(groupBy.replace('.', '_'));
		}
		System.out.println("RAGrouping finished setting up groupingAtts.");
	    
		/*
	    HashMap <String, AggFunc> myAggs = new HashMap <String, AggFunc> ();
	    myAggs.put ("att1", new AggFunc ("none", "Str(\"status: \") + o_orderstatus"));
	    myAggs.put ("att2", new AggFunc ("none", "Str(\"date: \") + o_orderdate"));
	    myAggs.put ("att3", new AggFunc ("avg", "o_totalprice * Int (100)"));
	    myAggs.put ("att4", new AggFunc ("sum", "Int (1)"));
	    */
		HashMap<String, AggFunc> myAggs = new HashMap<String, AggFunc>();
		for(int i = 0; i < select.size(); i++)
		{
			Expression e = select.get(i);
			if (e.includesAggregateFunction())
				myAggs.put("att" + i, new AggFunc(e.getType(), e.toExecutionString()));
			else
				myAggs.put("att" + i, new AggFunc("none", e.toExecutionString()));
		}
	    
		/*
	    // run the selection operation
	    try {
	      Grouping foo = new Grouping (inAtts, outAtts, groupingAtts, myAggs, "orders.tbl", "out.tbl", "g++", "cppDir/");
	    } catch (Exception e) {
	      throw new RuntimeException (e);
	    }
	    */
		
		try
		{
			new Grouping(inAtts, outAtts, groupingAtts, myAggs, inputFile, outputFile, DBConfig.CPP_COMPILER, DBConfig.CPP_TEMP_DIR);
		}
		catch(Exception e)
		{
			throw new RuntimeException(e);
		}
		
		// This has been moved to Grouping
//		if(inputRA != null && !inputFile.endsWith(".tbl"))
//		{
//			File f = new File(inputFile);
//			f.delete();
//		}
		
		return new RAOutput(outputFile, outAtts);
	}

	@Override
	public <T> T accept(RAExpressionVisitor<T> v, Object... args)
	{
		return v.groupingCase(this, args);
	}
	
	public RAExpression getInput()
	{
		return inputRA;
	}

	@Override
	public RAStatistics estimateCost(Map<String, TableData> catalog, Map<String, TableData> tempCatalog)
	{
		// In order to estimate the cost for an RAGrouping we need to compute the cost for its input RA.
		// After that the cardinality of the grouping attribute in the result set of the input RA is the
		// estimated cardinality of this RAGrouping.
		
//		long inputCard = 0;
		long inputCost = 0;
		if(this.inputRA != null)
		{
			RAStatistics inputStats = inputRA.estimateCost(catalog, tempCatalog);
//			inputCard = inputStats.getCardinality();
			inputCost = inputStats.getCost();
		}
		
		// Once we have the cardinality of the input, we need to look up the cardinality of the grouping attribute.
		if(this.groupBy == null)
		{
			return new RAStatistics(1, inputCost + 1);
		}
		
		CorrelatedAttribute groupByCa = CorrelatedAttribute.parseCorrelatedAttribute(groupBy);
		String correlation = groupByCa.getCorrelation();
		String attribute = groupByCa.getAttribute();
		String table = from.get(correlation);
		
		// Get the cardinality of the group by attribute.
		TableData td = tempCatalog.get(table);
		if(td == null)
		{
			td = catalog.get(table).clone();
			tempCatalog.put(table, td);
		}
		
		long groupByCard = td.getAttInfo(attribute).getNumDistinctVals();
		
		// If group by were not the final step in every query, then all other attributes in the result set will
		// have cardinalities of 1, so update their catalog entries in the temporary catalog would have to be updated.
		// As this is not the case, it is ok to not update them for now.
		
		DBUtils.setCatalogMaxCard(tempCatalog, groupByCard);
		
		return new RAStatistics(groupByCard, groupByCard + inputCost);
	}

	@Override
	public void getTables(Map<String, String> tables)
	{
		if(inputRA == null)
		{
			tables.putAll(from);
		}
		else
		{
			inputRA.getTables(tables);
		}
	}

	@Override
	public ArrayList<RAExpression> getJoinTransformations()
	{
		if(inputRA == null)
		{
			return null;
		}
		
		ArrayList<RAExpression> transformations = inputRA.getJoinTransformations();
		if(transformations == null)
		{
			return null;
		}
		
		for(int i = 0; i < transformations.size(); i++)
		{
			RAGrouping clone = this.clone();
			clone.inputRA = transformations.get(i);
			transformations.set(i, clone);
		}
		
		return transformations;
	}

	@Override
	public boolean referencedByExpression(Expression e)
	{
		if(inputRA != null)
		{
			return inputRA.referencedByExpression(e);
		}
		else
		{
			String correlation = (String) from.keySet().toArray()[0];
			return e.referencesTable(correlation);
		}
	}

	@Override
	public void addWhereDisjunction(Expression e)
	{
		throw new RuntimeException("Group by queries do not yet support having where disjunctions added!");
	}

	@Override
	public String joinPlanToString()
	{
		if(inputRA != null)
		{
			String subPlan = inputRA.joinPlanToString();
			return "GROUP BY(" + subPlan + ")";
		}
		else
		{
			String table = (String) from.keySet().toArray()[0];
			return "GROUP BY(" + table + ")";
		}
	}
	
	public String toString()
	{
		return joinPlanToString();
	}

	/**
	 * JES TODO: This is a poorly designed method since I'm not using the selectList parameter.
	 */
	@Override
	public void pushdownReferencedColumnsList(ArrayList<Expression> selectList)
	{
		if(inputRA != null)
		{
			inputRA.pushdownReferencedColumnsList(selectList);
		}
	}

	@Override
	public void applySelectList(ArrayList<Expression> select)
	{
		// TODO: Currently a group by query is always going to apply its select list.
		// TODO: Update Group by so that it can work with order by.
	}

	@Override
	public void pushdownJoinPredicates(ArrayList<Expression> joinWhereClauses)
	{
		if(this.inputRA != null)
		{
			inputRA.pushdownJoinPredicates(joinWhereClauses);
		}
	}

}
